Skip to content

BEAM-native JS engine and compiler#5

Open
dannote wants to merge 574 commits intomasterfrom
beam-vm-interpreter
Open

BEAM-native JS engine and compiler#5
dannote wants to merge 574 commits intomasterfrom
beam-vm-interpreter

Conversation

@dannote
Copy link
Copy Markdown
Member

@dannote dannote commented Apr 15, 2026

Adds a second QuickJS execution backend on the BEAM.

What’s in here

  • QuickJS bytecode decoder in Elixir
  • interpreter for QuickJS bytecode on the BEAM
  • hybrid compiler from QuickJS bytecode to BEAM modules
  • raw BEAM disassembly for the :beam backend via QuickBEAM.disasm/2
  • mode: :beam support in the public API
  • require(), module loading, dynamic import, globals, handlers, and interop for the VM path
  • stack traces, source positions, and Error.captureStackTrace

Runtime coverage

  • Object, Array, Function, String, Number, Boolean
  • Math, JSON, Date, RegExp
  • Map, Set, WeakMap, WeakSet, Symbol
  • Promise, async/await, generators, async generators
  • Proxy, Reflect
  • TypedArray, ArrayBuffer, BigInt
  • classes, inheritance, super, private fields, private methods, private accessors, static private members, brand checks

Validation

  • QUICKBEAM_BUILD=1 MIX_ENV=test mix test
  • MIX_ENV=test QUICKBEAM_BUILD=1 mix test test/vm/js_engine_test.exs --include js_engine --seed 0
  • mix compile --warnings-as-errors
  • mix format --check-formatted
  • mix credo --strict
  • mix dialyzer
  • mix ex_dna
  • zlint lib/quickbeam/*.zig lib/quickbeam/napi/*.zig
  • bunx oxlint -c oxlint.json --type-aware --type-check priv/ts/
  • bunx jscpd lib/quickbeam/*.zig priv/ts/*.ts --min-tokens 50 --threshold 0

Current local result:

  • 2363 tests, 0 failures, 1 skipped, 54 excluded

@dannote dannote force-pushed the beam-vm-interpreter branch from 0eb3475 to 7c1c574 Compare April 15, 2026 14:06
@dannote dannote changed the title BEAM-native JS interpreter (Phase 0-1) BEAM-native JS interpreter Apr 16, 2026
@dannote dannote marked this pull request as ready for review April 16, 2026 08:41
@dannote dannote force-pushed the beam-vm-interpreter branch 2 times, most recently from 75fdba5 to 527d5b9 Compare April 20, 2026 08:45
@dannote dannote changed the title BEAM-native JS interpreter BEAM-native JS engine and compiler Apr 21, 2026
dannote added 30 commits April 24, 2026 19:18
test262: 87 failures (94.5%), 82s runtime
Session progress: 91 → 87 = 4 tests fixed
Array.prototype[Symbol.iterator] fix resolved the iter-get-err tests.
try/finally double-execution investigation ongoing.

94.5% pass rate (1,495 / 1,582)
Fix catch_js_throw and catch_js_throw_refresh_globals to use
explicit try/catch around fun.() only, preventing the function-
level catch from intercepting throws from run(pc+1,...) continuation.

This is a correctness fix that prevents accidental catch of throws
from subsequent opcode execution. Combined with the Array.prototype
Symbol.iterator fix, resolves 4 additional tests.

test262: 87 failures (94.5% pass rate, 1,495 / 1,582)
Separate Put.put from run(pc+1,...) in put_field's try/catch block.
Previously, the try block wrapped both the operation AND the
continuation, causing throws from subsequent opcodes to be caught
with a stale ctx and re-dispatched via throw_or_catch. This caused
the finally block to execute twice when the catch body re-threw.

Fix: wrap only Put.put in try, use :ok/{:throw,error} result tuple
for branching. The continuation run(pc+1,...) executes AFTER the
try block, so subsequent throws propagate naturally to the function-
level catch_js_throw_refresh_globals handler.

test262: 87 → 85 (2 tests fixed: completion-values-fn-finally)
94.6% pass rate (1,497 / 1,582)
Custom iterators on objects now get correct 'this' binding when
called from for..of loops. Fixes non-generator custom iterators.
Generator this binding is a separate issue.

Also fix invoke_custom_iterator in both interpreter and compiled
runtime_helpers to use invoke_with_receiver.

test262: 85 failures (94.6% pass rate, 1,497 / 1,582)
Compiled path's compiled_gen_invoke doesn't preserve 'this' in
generator continuations. Fix: compiled_method_callable? only
returns true for func_kind=0 (regular functions), forcing
generators/async to use the interpreter path which correctly
passes 'this' through the context.

test262: 85 → 81 (4 tests fixed: all iter-val-array-prototype)
94.9% pass rate (1,501 / 1,582)
1. Bytecode decoder: encode lone surrogates (U+D800-U+DFFF) as
   CESU-8 (3-byte) instead of returning error tuples. This allows
   JS strings with unpaired surrogates to be stored as binaries.

2. String comparison: implement utf16_compare that converts strings
   to UTF-16 code unit sequences for comparison. JS compares by
   UTF-16 code units, not UTF-8 bytes. Supplementary characters
   (U+10000+) encode as surrogate pairs (D800-DBFF) which sort
   BEFORE BMP chars in UTF-16 but AFTER in UTF-8.

3. Fast path: strings without bytes >= 0xED use direct binary
   comparison (UTF-8 and UTF-16 give same order for BMP-only).

test262: 81 → 77 (4 tests fixed: all surrogate comparison tests)
95.1% pass rate (1,505 / 1,582)
1. instanceof: use Get.get for Symbol.hasInstance (invokes accessor
   getters from defineProperty). Use catch_js_throw_refresh_globals
   for globals propagation. Check {:obj,_} callable via 'call' property.

2. Unicode: CESU-8 encoding for lone surrogates + UTF-16 code unit
   comparison for strings with supplementary characters.

3. Generator this: compiled_method_callable only for func_kind=0
   (regular functions), generators use interpreter path.

4. Function.prototype.constructor set during globals init.

5. Array.prototype[Symbol.iterator]: check for deletion/override,
   invoke_with_receiver for this binding, separate put_field try/catch.

test262: 91 → 75 (16 tests fixed this session)
95.3% pass rate (1,507 / 1,582)
When closures have accessor properties set via Object.defineProperty
(stored in ctor_statics), get_own now invokes the getter instead of
returning the raw {:accessor, ...} tuple.

test262: 75 failures (95.3% pass rate, 1,507 / 1,582)
Use Get.get instead of raw Map.get for reading 'done' and 'value'
from iterator result objects. This properly invokes accessor getters
set via Object.defineProperty on iterator results.

test262: 75 → 73 (2 tests fixed: spread-err-*-itr-value timeout)
95.4% pass rate (1,509 / 1,582)
Values.shr matched shr(_, {:bigint, _}) before shr({:obj, _}, b),
causing TypeError to fire before ToPrimitive could throw the
correct error. Move object coercion clauses before BigInt clauses.

test262: 73 → 72 (bigint-toprimitive.js fixed)
95.4% pass rate (1,510 / 1,582)
Result: {"status":"keep","failures":72,"passing_tests":1510,"skipped_tests":912}
…ch handlers, extract for_of_start throws to throw_or_catch

Result: {"status":"keep","failures":68,"passing_tests":1514,"skipped_tests":912}
Result: {"status":"keep","failures":68,"passing_tests":1514,"skipped_tests":912}
…ith configurable:false

Result: {"status":"keep","failures":67,"passing_tests":1515,"skipped_tests":912}
…port correct constructor identity

Result: {"status":"keep","failures":66,"passing_tests":1516,"skipped_tests":912}
…rget callability

Result: {"status":"keep","failures":65,"passing_tests":1517,"skipped_tests":912}
…totype for list/qb_arr heap data

Result: {"status":"keep","failures":64,"passing_tests":1518,"skipped_tests":912}
…lass fields, array proto setters, for-in member LHS)

Result: {"status":"keep","failures":64,"passing_tests":1518,"skipped_tests":912}
…ptured_locals for shared cell refs

Result: {"status":"keep","failures":63,"passing_tests":1519,"skipped_tests":912}
Result: {"status":"keep","failures":62,"passing_tests":1520,"skipped_tests":912}
…er changes.

Result: {"status":"keep","failures":62,"passing_tests":1520,"skipped_tests":912}
…ototype for accessors on OOB indices, put_array_el refreshes persistent globals after

Result: {"status":"keep","failures":61,"passing_tests":1521,"skipped_tests":912}
…ll with-statement scope (unfixable without deep refactor).

Result: {"status":"keep","failures":61,"passing_tests":1521,"skipped_tests":912}
…/put_ref_value stack format. Eliminates all 23 unimplemented_opcode crashes.

Result: {"status":"keep","failures":61,"passing_tests":1521,"skipped_tests":912}
…rmats. Eliminates 23 opcode crashes in with-scope tests (no net test improvement but tests progress further).

Result: {"status":"keep","failures":61,"passing_tests":1521,"skipped_tests":912}
… invoking getter side effects during property existence check. -14 failures!

Result: {"status":"keep","failures":47,"passing_tests":1535,"skipped_tests":912}
… fix + 9 via make_*_ref 2-value push. 96.9% pass rate.

Result: {"status":"keep","failures":47,"passing_tests":1535,"skipped_tests":912}
…var_ref cells now update ctx.globals and globalThis. -21 failures!

Result: {"status":"keep","failures":26,"passing_tests":1556,"skipped_tests":912}
…sting keys). Fixes p5 creation inside with-scope. -12 failures!

Result: {"status":"keep","failures":14,"passing_tests":1568,"skipped_tests":912}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant